package org.eclipse.gittab.player.internal.midi;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;
import javax.sound.midi.Transmitter;

import org.eclipse.gittab.gitTabDsl.Fret;
import org.eclipse.gittab.gitTabDsl.GitTabDslFactory;
import org.eclipse.gittab.gitTabDsl.GuitarStr;
import org.eclipse.gittab.gitTabDsl.Note;
import org.eclipse.gittab.gitTabDsl.Tuning;
import org.eclipse.gittab.player.ComposedTab;
import org.eclipse.gittab.player.Player;
import org.eclipse.gittab.util.GitTabUtil;

public class MidiSequencePlayer implements Player {

	/**
	 * This receiver notifies all listeners.
	 * 
	 * @author mtreitle
	 * 
	 */
	class NotificationReceiver implements Receiver {

		@Override
		public void close() {
		}

		@Override
		public void send(MidiMessage message, long timeStamp) {

			if (tuning != null) {
				if (message instanceof ShortMessage) {

					ShortMessage msg = (ShortMessage) message;
					int channel = msg.getChannel();
					int cmd = msg.getCommand();
					int midi = msg.getData1();
					// int velocity = msg.getData2(); unused

					// only be interested in note on and note off events
					if (cmd == ShortMessage.NOTE_ON
							|| cmd == ShortMessage.NOTE_OFF) {

						if (channel < tuning.getStrings().size()) {
							GuitarStr string = GitTabUtil.int2GuitarString(
									channel, tuning);

							int fret = MidiMapper.TypeRegistry.CACHE
									.getFretFrom(midi, string);

							Note note = GitTabDslFactory.eINSTANCE.createNote();
							note.setGitString(string);
							Fret wrapper = GitTabDslFactory.eINSTANCE
									.createFret();
							wrapper.setFret(fret);
							note.setNote(wrapper);

							fireEvent(note, cmd == ShortMessage.NOTE_ON);
						}
					}
				}
			}
		}
	}

	public static final int DEF_BPM = 100;

	private Sequencer sequencer;
	private int tempo = DEF_BPM;

	private Tuning tuning;

	// safe listener list
	private final CopyOnWriteArrayList<PlayerListener> listeners = new CopyOnWriteArrayList<PlayerListener>();

	public MidiSequencePlayer() {

		try {
			sequencer = MidiSystem.getSequencer(false);

			Synthesizer synth = MidiSystem.getSynthesizer();
			synth.open();
			Transmitter synthTransmitter = sequencer.getTransmitter();
			synthTransmitter.setReceiver(synth.getReceiver());

			Transmitter seqTransmitter = sequencer.getTransmitter();
			seqTransmitter.setReceiver(new NotificationReceiver());

		} catch (MidiUnavailableException e) {
			e.printStackTrace();
		}

		open();
	}

	public final void fireEvent(List<Note> notes, boolean played) {

		for (PlayerListener l : listeners) {
			if (played)
				l.noteOn(notes);
			else
				l.noteOff(notes);
		}
	}

	public final void fireEvent(Note note, boolean played) {

		for (PlayerListener l : listeners) {
			if (played)
				l.noteOn(Collections.singletonList(note));
			else
				l.noteOff(Collections.singletonList(note));
		}
	}

	@Override
	public void addPlayerListener(PlayerListener listener) {
		listeners.add(listener);
	}

	@Override
	public void removePlayerListener(PlayerListener listener) {
		listeners.remove(listener);
	}

	public void open() {
		try {
			sequencer.open();
		} catch (MidiUnavailableException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void close() {
		sequencer.close();
	}

	public void dispose() {
		close();
	}

	@Override
	public void load(ComposedTab tab, Tuning tuning) {

		if (tab.getTab() instanceof Sequence) {

			this.tuning = tuning;

			Sequence sequence = (Sequence) tab.getTab();

			load(sequence);
		}
	}

	protected void load(Sequence sequence) {

		if (!sequencer.isOpen()) {
			open();
		}

		try {
			sequencer.setSequence(sequence);
		} catch (InvalidMidiDataException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void play() {
		check();
		sequencer.start();
	}

	@Override
	public long position() {

		check();
		return sequencer.getMicrosecondPosition();
	}

	@Override
	public long length() {

		check();
		return sequencer.getMicrosecondLength();
	}

	@Override
	public void stop() {

		check();
		sequencer.stop();
	}

	@Override
	public void tempo(int bpm) {
		check();
		sequencer.setTempoInBPM(bpm);

		this.tempo = bpm;
	}

	@Override
	public void tempoByFactor(float factor) {
		check();
		sequencer.setTempoFactor(factor);

		this.tempo = (int) sequencer.getTempoInBPM();
	}

	protected void check() throws IllegalStateException {

		if (sequencer == null || !sequencer.isOpen()) {
			throw new IllegalStateException("Sequencer not ready.");
		}
	}

	@Override
	public int getTempo() {
		return tempo;
	}
}
